package se.thoka.twt.client.utils;

import java.util.Date;


/**
 * @author Thobias Karlsson, Thoka
 * @since 9 jan 2010
 */
@SuppressWarnings("deprecation")
public class DateTimeUtil
{
	/**
	 * Sum of the number of days in the months.
	 */
	private static int[] DAYNUMBERS = new int[]{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};

	/**
	 * The month of January
	 */
	public static final int JANUARY = 1;
	/**
	 * The month of February
	 */
	public static final int FEBRUARY = 2;
	/**
	 * The month of March 
	 */
	public static final int MARCH = 3;
	/**
	 * The month of April 
	 */
	public static final int APRIL = 4;
	/**
	 * The month of May 
	 */
	public static final int MAY = 5;
	/**
	 * The month of June 
	 */
	public static final int JUNE = 6;
	/**
	 * The month of July 
	 */
	public static final int JULY = 7;
	/**
	 * The month of August 
	 */
	public static final int AUGUST = 8;
	/**
	 * The month of September 
	 */
	public static final int SEPTEMBER = 9;
	/**
	 * The month of October 
	 */
	public static final int OCTOBER = 10;
	/**
	 * The month of November 
	 */
	public static final int NOVEMBER = 11;
	/**
	 * The month of December 
	 */
	public static final int DECEMBER = 12;
	/**
	 * The first day of the month.
	 */
	public static final int FIRSTDAY = 1;
	/**
	 * The last day of the month.
	 */
	public static final int LASTDAY = 31;
	
	/**
	 * Sunday
	 */
	public static final int SUNDAY = 0;
	/**
	 * Monday
	 */
	public static final int MONDAY = 1;
	/**
	 * Tuesday
	 */
	public static final int TUESDAY = 2;
	/**
	 * Wednesday
	 */
	public static final int WEDNESDAY = 3;
	/**
	 * Thursday
	 */
	public static final int THURSDAY = 4;
	/**
	 * Friday
	 */
	public static final int FRIDAY = 5;
	/**
	 * Saturday
	 */
	public static final int SATURDAY = 6;
	/**
	 * Number days in week constant
	 */
	public static final int DAYS_IN_WEEK = 7;
	
	public static final long SECOND_IN_MILLIS = 1000;
	public static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
	public static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
	public static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
	public static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
	
	/**
	 * The date.
	 */
	private Date date;
	
	/**
	 * Creates a new instance of <code>DateTimeUtil</code>.
	 * @param date The date.
	 */
	public DateTimeUtil(Date date)
	{
		this.date = date;
	}
	
	/**
	 * Creates a new instance of <code>DateTimeUtil</code> at todays date.
	 */
	public DateTimeUtil()
	{
		this(new Date());
	}
	
	/**
	 * Creates a new instance of <code>DateTimeUtil</code>.
	 * @param year The year.
	 * @param monthOfYear The month of year, e.g. FEBRUARY.
	 * @param dayOfMonth The day of month.
	 */
	public DateTimeUtil(int year, int monthOfYear, int dayOfMonth)
	{
		super();
		int n = getDaysInMonth(year, monthOfYear);
		if (dayOfMonth > n)
		{
			dayOfMonth = n;
		}
		this.date = new Date(year - 1900, monthOfYear - 1, dayOfMonth, 12, 0, 0);
	}

	/**
	 * Creates a new instance of <code>DateTimeUtil</code>.
	 * @param date A {@link GWTDate}.
	 */
	public DateTimeUtil(GWTDate date)
	{
		this(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth());
	}
	
	/**
	 * Creates a copy of a <code>DateTimeUtil</code>.
	 * @param dTU A {@link DateTimeUtil}
	 */
	public DateTimeUtil(DateTimeUtil dTU)
	{
		this(dTU.getYear(), dTU.getMonthOfYear(), dTU.getDayOfMonth());
	}
	
	/**
	 * Returns the date.
	 * @return The date.
	 */
	public Date getDate()
	{
		return (Date) date.clone();
	}
	
	/**
	 * Increments the date time util with one day.
	 */
	public void addDay()
	{
		this.date = new Date(date.getYear(), date.getMonth(), date.getDate() + 1, date.getHours(), date.getMinutes());
	}
	
	/**
	 * Returns the day of month.
	 * @return Returns the day of month.
	 */
	public int getDayOfMonth()
	{
		return date.getDate();
	}

	/**
	 * Returns the month of year.
	 * @return Returns the month of year.
	 */
	public int getMonthOfYear()
	{
		return date.getMonth() + 1;
	}

	/**
	 * Returns the year.
	 * @return Returns the year.
	 */
	public int getYear()
	{
		return date.getYear() + 1900;
	}

	/**
	 * Returns the day of week.
	 * @return The day of week, e.g. SUNDAY.
	 */
	public int getDayOfWeek()
	{
		return date.getDay();
	}
	
	/**
	 * Returns the day of the year.
	 * @return The day of the year.
	 */
	public int getDayOfYear()
	{
		return getDayOfYear(leap(getYear()));
	}
	
	/**
	 * Returns the day of the year.
	 * @param leap Is it a leap year?
	 * @return the day of the year.
	 */
	private int getDayOfYear(boolean leap)
	{
		int result = DAYNUMBERS[date.getMonth()] + getDayOfMonth();
		if (leap)
		{
			result += 1;
		}
		return result;
	}
	
	/**
	 * Leap year?
	 * @param Y The year.
	 * @return true if a leap year.
	 */
	private static boolean leap(int Y)
	{
		return (Y % 4 == 0  &&  Y % 100 != 0) || Y % 400 == 0;
	}
	
	/**
	 * Returns the maximum number of weeks in the
	 * current year, i.e. 53 for 2009.
	 * @return The maximum number of weeks in this
	 * 	year, e.g. 53 for the year 2009.
	 */
	public int getMaxWeekOfYear()
	{
		int w53 = 0;
		if ((new Date(getYear() - 1900, 0, 1)).getDay() == 4 || (new Date(getYear() - 1900, 11, 31)).getDay() == 4)
		{
			w53 = 1;
		}
		return 52 + w53;
	}
	
	/**
	 * Returns the week of year (ISO 8601). The variable names
	 * in this method are messed up because they are taken straight
	 * from the actual algorithm example.
	 * 
	 * @return The week of year.
	 */
	public int getWeekOfYear()
	{
		// 1 
		int Y = getYear();
		int M = getMonthOfYear();
		int D = getDayOfMonth();
		// 2
		boolean leap0 = leap(Y);
		// 3
		boolean leap1 = leap(Y - 1);
		// 4
		int DayOfYearNumber = D + DAYNUMBERS[M - 1] + ((leap0 && M > 2) ? 1 : 0);
		// 5
		int YY = (Y - 1) % 100;
		int C = (Y - 1) - YY;
		int G = YY + YY / 4;
		int Jan1Weekday = 1 + (((((C / 100) % 4) * 5) + G) % 7);
		// 6
		int H = DayOfYearNumber + (Jan1Weekday - 1);
		int Weekday = 1 + ((H -1) % 7);
		// 7
		int YearNumber;
		int WeekNumber = -1;
		if (DayOfYearNumber <= (8 - Jan1Weekday) && Jan1Weekday > 4)
		{
			YearNumber = Y - 1;
			if (Jan1Weekday == 5 || (Jan1Weekday == 6 && leap1))
			{	
				WeekNumber = 53;
			}
			else
			{
				WeekNumber = 52;
			}
		}
		else
		{
			YearNumber = Y;
		}
		// 8
		if (YearNumber == Y)
		{
			int I = (leap0 ? 366 : 365);
	        if ((I - DayOfYearNumber) < (4 - Weekday))
	        {
	              YearNumber = Y + 1;
	              WeekNumber = 1;
	        }
		}
		// 9
		if (YearNumber == Y)
		{
	         int J = DayOfYearNumber + (7 - Weekday) + (Jan1Weekday -1);
	         WeekNumber = J / 7;
	         if (Jan1Weekday > 4)
	         {
	        	 WeekNumber -= 1;
	         }
		}
		return WeekNumber;
	}
	
	/**
	 * Returns a DateTimeUtil at the next day of week.
	 * @param dayOfWeek The requested day of week.
	 * @return A DateTimeUtil.
	 */
	public DateTimeUtil getPreviousDayOfWeek(int dayOfWeek)
	{
		if (getDayOfWeek() == dayOfWeek)
		{
			return new DateTimeUtil(date);
		}
		Date d = new Date(date.getTime());
		while (d.getDay() != dayOfWeek)
		{
			int year = d.getYear();
			int day = d.getDate() - 1;
			int month = d.getMonth();
			if (day < FIRSTDAY)
			{
				month = month - 1;
				if (month < 0)
				{
					month = 11;
					year = year - 1;
				}
				day = getDaysInMonth(year + 1900, month + 1);	
			}
			d = new Date(year, month, day);
		}
		return new DateTimeUtil(d);
	}
	
	/**
	 * Returns the next day of week.
	 * @param dayOfWeek The day of week.
	 * @return A DateTimeUtil.
	 */
	public DateTimeUtil getNextDayOfWeek(int dayOfWeek)
	{
		if (getDayOfWeek() == dayOfWeek)
		{
			return new DateTimeUtil(date);
		}
		Date d = new Date(date.getTime());
		while (d.getDay() != dayOfWeek)
		{
			int year = d.getYear();
			int day = d.getDate() + 1;
			int month = d.getMonth();
			if (day > 27)
			{
				int mx = getDaysInMonth(year + 1900, month + 1);
				if (day > mx)
				{
					month = month + 1;
					if (month > 11)
					{
						year = year + 1;
						month = 0;
					}
					day = FIRSTDAY;
				}
			}
			d = new Date(year, month, day, d.getHours(), d.getMinutes());
		}
		return new DateTimeUtil(d);
	}
	
	/**
	 * Returns the end of the week.
	 * @param startOfWeek The start of the week, e.g. SUNDAY.
	 * @return The end of the week, e.g. SATURDAY.
	 */
	public static int getEndOfWeek(int startOfWeek)
	{
		return (startOfWeek + DAYS_IN_WEEK - 1) % DAYS_IN_WEEK;
	}
	
	/**
	 * Returns a {@link GWTDate} bean containing the date of the
	 * <code>DateTimeUtil</code>.
	 * @return A {@link GWTDate} bean containing the current
	 * 	date of this <code>DateTimeUtil</code>. Must not return <code>null</code>.
	 */
	public GWTDate getGWTDate()
	{
		return new GWTDate(getYear(), getMonthOfYear(), getDayOfMonth());
	}
	
	/**
	 * Returns a {@link GWTTime} bean containing the time of day of the
	 * <code>DateTimeUtil</code>.
	 * @return A {@link GWTTime} bean containing the current time of
	 * 	day of this <code>DateTimeUtil</code>. Must not return <code>null</code>.
	 */
	public GWTTime getGWTTime()
	{
		return new GWTTime(date.getHours(), date.getMinutes(), date.getSeconds(), 0);
	}
	
	/**
	 * Returns a {@link GWTDateTime} bean containing the datetime of the
	 * <code>DateTimeUtil</code>.
	 * @return A {@link GWTDateTime} bean containing the current date and time
	 * 	of this <code>DateTimeUtil</code>. Must not return <code>null</code>.
	 */
	public GWTDateTime getGWTDateTime()
	{
		return new GWTDateTime(getGWTDate(), getGWTTime());
	}
	
	/**
	 * Returns the number of days in the supplied month
	 * of year. This mehtod should understand leap years.
	 * @param year The year, e.g. 1989.
	 * @param monthOfYear The month of the year, e.g. FEBRUARY.
	 * @return The number of days in the month of that year.
	 */
	public static int getDaysInMonth(int year, int monthOfYear)
	{
		int result = 31; 
		switch (monthOfYear)
		{
			case FEBRUARY:
				result = 28;
				if (leap(year))
				{
					result = 29;
				}
				break;
			case APRIL:
			case JUNE:
			case SEPTEMBER:
			case NOVEMBER:
				result = 30;
		}
		return result;
	}
	
	/**
	 * Increments the supplied date with the supplied number of months.
	 * @param date The date.
	 * @param numberOfMonths The number of months.
	 * @return An incremented date.
	 */
	public static Date incrementMonths(Date date, int numberOfMonths)
	{
		int m = date.getMonth() + numberOfMonths;
		int y = date.getYear();
		if (m < 0)
		{
			while (m < 0)
			{
				m += 12;
				y--;
			}
		}
		else if (m > 11)
		{
			while (m > 11)
			{
				m -= 12;
				y++;
			}
		}
		
		int d = date.getDate();
		int lastDayNewMonth = getLastDayOfMonth(y, m + 1);
		if (d > lastDayNewMonth)
			d = lastDayNewMonth;
		return date(y, m, d, date.getHours(), date.getMinutes(), date.getSeconds());
	}
	
	/**
	 * Increments the supplied date with the supplied number of years.
	 * @param date The date.
	 * @param numberOfYears The number of years.
	 * @return An incremented date.
	 */
	public static Date incrementYears(Date date, int numberOfYears)
	{
		return date(date.getYear() + numberOfYears, date.getMonth(), date.getDate(), 
				date.getHours(), date.getMinutes(), date.getSeconds());
	}
	
	/**
	 * Increments the supplied date with the supplied number of days.
	 * @param date The date.
	 * @param numberOfDays The number of days.
	 * @return An incremented date.
	 */
	public static Date incrementDays(Date date, int numberOfDays)
	{
		return date(date.getYear(), date.getMonth(), date.getDate() + numberOfDays, date.getHours(), date.getMinutes(), date.getSeconds());
	}
	
	/**
	 * Increments the supplied date with the supplied number of hours.
	 * @param date The date.
	 * @param numberOfHours The number of hours.
	 * @return An incremented date.
	 */
	public static Date incrementHours(Date date, int numberOfHours)
	{
		return date(date.getYear(), date.getMonth(), date.getDate(), date.getHours() + numberOfHours, date.getMinutes(), date.getSeconds());
	}
	
	/**
	 * Increments the supplied date with the supplied number of minutes.
	 * @param date The date.
	 * @param numberOfMinutes The number of minutes.
	 * @return An incremented date.
	 */
	public static Date incrementMinutes(Date date, int numberOfMinutes)
	{
		return date(date.getYear(), date.getMonth(), date.getDate(), date.getHours(), 
				date.getMinutes() + numberOfMinutes, date.getSeconds());
	}
	
	/**
	 * Increments the supplied date with the supplied number of years.
	 * @param dateTime The date.
	 * @param numberOfYears The number of years.
	 * @return An incremented date.
	 */
	public static GWTDateTime incrementYears(GWTDateTime dateTime, int numberOfYears)
	{
		return new GWTDateTime(incrementYears(dateTime.toDate(), numberOfYears));
	}
	
	/**
	 * Increments the supplied date with the supplied number of months.
	 * @param dateTime The date.
	 * @param numberOfMonths The number of months.
	 * @return An incremented date.
	 */
	public static GWTDateTime incrementMonths(GWTDateTime dateTime, int numberOfMonths)
	{
		return new GWTDateTime(incrementMonths(dateTime.toDate(), numberOfMonths));
	}
	
	/**
	 * Increments the supplied date with the supplied number of days.
	 * @param dateTime The date.
	 * @param numberOfDays The number of days.
	 * @return An incremented date.
	 */
	public static GWTDateTime incrementDays(GWTDateTime dateTime, int numberOfDays)
	{
		return new GWTDateTime(incrementDays(dateTime.toDate(), numberOfDays));
	}
	
	/**
	 * Increments the supplied date with the supplied number of hours.
	 * @param dateTime The date.
	 * @param numberOfHours The number of hours.
	 * @return An incremented date.
	 */
	public static GWTDateTime incrementHours(GWTDateTime dateTime, int numberOfHours)
	{
		return new GWTDateTime(incrementHours(dateTime.toDate(), numberOfHours));
	}
	
	/**
	 * Increments the supplied date with the supplied number of minutes.
	 * @param dateTime The date.
	 * @param numberOfMinutes The number of minutes.
	 * @return An incremented date.
	 */
	public static GWTDateTime incrementMinutes(GWTDateTime dateTime, int numberOfMinutes)
	{
		return new GWTDateTime(incrementMinutes(dateTime.toDate(), numberOfMinutes));
	}
	
	/**
	 * Increments the supplied date with the supplied number of years.
	 * @param date The date.
	 * @param numberOfYears The number of years.
	 * @return An incremented date.
	 */
	public static GWTDate incrementYears(GWTDate date, int numberOfYears)
	{
		return new GWTDate(incrementYears(date.toDate(), numberOfYears));
	}
	
	/**
	 * Increments the supplied date with the supplied number of months.
	 * @param date The date.
	 * @param numberOfMonths The number of months.
	 * @return An incremented date.
	 */
	public static GWTDate incrementMonths(GWTDate date, int numberOfMonths)
	{
		return new GWTDate(incrementMonths(date.toDate(), numberOfMonths));
	}
	
	/**
	 * Increments the supplied date with the supplied number of days.
	 * @param date The date.
	 * @param numberOfDays The number of days.
	 * @return An incremented date.
	 */
	public static GWTDate incrementDays(GWTDate date, int numberOfDays)
	{
		return new GWTDate(incrementDays(date.toDate(), numberOfDays));
	}
	
	/**
	 * Creates & returns the supplied date.
	 * @param year The year.
	 * @param month The month.
	 * @param date The date.
	 * @param hours The hours.
	 * @param minutes The minutes.
	 * @param seconds The seconds.
	 * @return A new date.
	 */
	protected static Date date(int year, int month, int date, int hours, int minutes, int seconds)
	{
		return new Date(year, month, date, hours, minutes, seconds);
	}
	
	/**
	 * Increments the date time util with one day.
	 * @param date The date.
	 * @return An incremented date.
	 */
	public static Date incrementOneDay(Date date)
	{
		return incrementDays(date, 1);
	}
	
	/**
	 * Increments the date time util with one day.
	 * @param date The date.
	 * @return An incremented date.
	 */
	public static GWTDate incrementOneDay(GWTDate date)
	{
		return incrementDays(date, 1);
	}
	
	/**
	 * Increments the date time util with one day.
	 * @param date The date.
	 * @return An incremented date.
	 */
	public static GWTDateTime incrementOneDay(GWTDateTime date)
	{
		return incrementDays(date, 1);
	}
	
	/**
	 * Returns the last day in month of year of the given date.
	 * @param date The date (containing month and year)
	 * @return The last day in month of year.
	 */
	public static int getLastDayOfMonth(GWTDate date)
	{
		return getLastDayOfMonth(date.getYear(), date.getMonthOfYear());
	}
	
	/**
	 * Returns the last day in month of year of the given date.
	 * @param date The date (containing month and year)
	 * @return The last day in month of year.
	 */
	public static int getLastDayOfMonth(Date date)
	{
		return getLastDayOfMonth(date.getYear(), date.getMonth());
	}
	
	/**
	 * Returns the last day in month of year.
	 * @param year The year.
	 * @param month The month (January = 1, February = 2 ...)
	 * @return The last day in month of year.
	 */
	public static int getLastDayOfMonth(int year, int month)
	{
		int lastDay = -1;
		switch (month)
		{
			case JANUARY:
			case MARCH:
			case MAY:
			case JULY:
			case AUGUST:
			case OCTOBER:
			case DECEMBER:
				lastDay = 31;
				break;
			case APRIL:
			case JUNE:
			case SEPTEMBER:
			case NOVEMBER:
				lastDay = 30;
				break;
			case FEBRUARY:
				lastDay = leap(year) ? 29 : 28;
				break;
		}
		return lastDay;
	}
	
	@Override
	public boolean equals(Object obj)
	{
		boolean result = false;
		if (obj != null && obj instanceof DateTimeUtil)
		{
			DateTimeUtil dTU = (DateTimeUtil) obj;
			result = dTU.getDate().equals(getDate());
		}
		return result;
	}

	@Override
	public int hashCode()
	{
		int result = 17;
		result = 31 * result + date.getYear();
		result = 31 * result + date.getMonth();
		result = 31 * result + date.getDay();
		result = 31 * result + date.getHours();
		result = 31 * result + date.getMinutes();
		result = 31 * result + date.getSeconds();
		return result;
	}
}
